﻿//
// (C) Copyright 2009 Irantha Suwandarathna (iranthas@hotmail.com)
// All rights reserved.
//

/* Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Reflection;
#if !SILVERLIGHT
using System.Data.Common;
#endif
using EffiProz.Core.Lib;
using EffiProz.Core.DataTypes;
using EffiProz.Core.Indexes;
using EffiProz.Core.Errors;
using EffiProz.Core.Persist;
using EffiProz.Core.Results;
using EffiProz.Core.Navigators;
using EffiProz.Core.Rights;
using EffiProz.Core.Store;

namespace EffiProz.Core
{
    public class Routine : SchemaObject
    {
        public const int NO_SQL = 1;
        public const int CONTAINS_SQL = 2;
        public const int READS_SQL = 3;
        public const int MODIFIES_SQL = 4;

        //
        public const int LANGUAGE_JAVA = 1;
        public const int LANGUAGE_SQL = 2;

        //
        public const int PARAM_STYLE_JAVA = 1;
        public const int PARAM_STYLE_SQL = 2;

        //
        public static Routine[] emptyArray = new Routine[] { };

        //
        public RoutineSchema routineSchema;
        private QNameManager.QName name;
        private QNameManager.QName specificName;
        public SqlType[] parameterTypes;
        public int typeGroups;
        public SqlType returnType;
        public SqlType[] tableType;
        public TableDerived returnTable;
        public int routineType;
        public int language = LANGUAGE_SQL;
        public int dataImpact = CONTAINS_SQL;
        public int parameterStyle;
        public bool _isDeterministic;
        public bool _isNullInputOutput;
        public bool isNewSavepointLevel = true;
        public bool _isPSM;
        public bool _returnsTable;
        public Statement statement;

        //
        private String methodName;
        public MethodInfo cSharpMethod;
        public bool javaMethodWithConnection;
        private bool _isLibraryRoutine;

        //
        public HashMappedList parameterList = new HashMappedList();
        public int scopeVariableCount;
        public RangeVariable[] ranges;

        //
        public int variableCount;

        //
        public Routine(int type)
        {
            routineType = type;
            returnType = SqlType.SQL_ALL_TYPES;
            ranges = new RangeVariable[]{
        new RangeVariable(parameterList, false) };
        }

        public virtual int getType()
        {
            return routineType;
        }

        public virtual QNameManager.QName getName()
        {
            return name;
        }

        public virtual QNameManager.QName getSchemaName()
        {
            return name.schema;
        }

        public virtual QNameManager.QName getCatalogName()
        {
            return name.schema.schema;
        }

        public virtual Grantee getOwner()
        {
            return name.schema.owner;
        }

        public virtual OrderedHashSet getReferences()
        {

            if (statement == null)
            {
                return new OrderedHashSet();
            }

            return statement.getReferences();
        }

        public virtual OrderedHashSet getComponents()
        {
            return null;
        }

        public virtual void compile(Session session, SchemaObject parentObject) { }

        public virtual String getSQL()
        {

            StringBuilder sb = new StringBuilder();

            sb.Append(Tokens.T_CREATE).Append(' ');

            if (routineType == SchemaObjectTypes.PROCEDURE)
            {
                sb.Append(Tokens.T_PROCEDURE);
            }
            else
            {
                sb.Append(Tokens.T_FUNCTION);
            }

            sb.Append(' ');
            sb.Append(name.getSchemaQualifiedStatementName());
            sb.Append('(');

            for (int i = 0; i < parameterList.size(); i++)
            {
                if (i > 0)
                {
                    sb.Append(',');
                }

                ColumnSchema param = (ColumnSchema)parameterList.get(i);

                // in - out
                sb.Append(param.getSQL());
            }

            sb.Append(')');
            sb.Append(' ');

            if (routineType == SchemaObjectTypes.FUNCTION)
            {
                sb.Append(Tokens.T_RETURNS);
                sb.Append(' ');
                sb.Append(returnType.getTypeDefinition());
                sb.Append(' ');
            }

            // SPECIFIC
            if (specificName != null)
            {
                sb.Append(Tokens.T_SPECIFIC);
                sb.Append(' ');
                sb.Append(specificName.getStatementName());
                sb.Append(' ');
            }

            //
            sb.Append(Tokens.T_LANGUAGE);
            sb.Append(' ');

            if (language == LANGUAGE_JAVA)
            {
                sb.Append(Tokens.T_JAVA);
            }
            else
            {
                sb.Append(Tokens.T_SQL);
            }

            sb.Append(' ');

            //
            if (!_isDeterministic)
            {
                sb.Append(Tokens.T_NOT);
                sb.Append(' ');
            }

            sb.Append(Tokens.T_DETERMINISTIC);
            sb.Append(' ');

            //
            sb.Append(getDataImpactString());
            sb.Append(' ');

            //
            if (routineType == SchemaObjectTypes.FUNCTION)
            {
                if (_isNullInputOutput)
                {
                    sb.Append(Tokens.T_RETURNS).Append(' ').Append(Tokens.T_NULL);
                }
                else
                {
                    sb.Append(Tokens.T_CALLED);
                }

                sb.Append(' ').Append(Tokens.T_ON).Append(' ');
                sb.Append(Tokens.T_NULL).Append(' ').Append(Tokens.T_INPUT);
                sb.Append(' ');
            }
            else
            {
                if (isNewSavepointLevel)
                {
                    sb.Append(Tokens.T_NEW);
                }
                else
                {
                    sb.Append(Tokens.T_OLD);
                }

                sb.Append(' ').Append(Tokens.T_SAVEPOINT).Append(' ');
                sb.Append(Tokens.T_LEVEL).Append(' ');
            }

            if (language == LANGUAGE_JAVA)
            {
                sb.Append(Tokens.T_EXTERNAL).Append(' ').Append(Tokens.T_NAME);
                sb.Append(' ').Append('\'').Append(methodName).Append('\'');
            }
            else
            {
                sb.Append(statement.getSQL());
            }

            return sb.ToString();
        }

        public void addParameter(ColumnSchema param)
        {

            QNameManager.QName name = param.getName();
            String paramName =
                name == null
                ? QNameManager.getAutoNoNameColumnString(parameterList.size())
                : name.name;

            parameterList.add(paramName, param);
        }

        public void setLanguage(int lang)
        {
            language = lang;
            _isPSM = language == LANGUAGE_SQL;
        }

        public int getLanguage()
        {
            return language;
        }

        public bool isPSM()
        {
            return _isPSM;
        }

        public void setDataImpact(int impact)
        {
            dataImpact = impact;
        }

        public int getDataImpact()
        {
            return dataImpact;
        }

        public String getDataImpactString()
        {

            StringBuilder sb = new StringBuilder();

            switch (this.dataImpact)
            {

                case NO_SQL:
                    sb.Append(Tokens.T_NO).Append(' ').Append(Tokens.T_SQL);
                    break;

                case CONTAINS_SQL:
                    sb.Append(Tokens.T_CONTAINS).Append(' ').Append(Tokens.T_SQL);
                    break;

                case READS_SQL:
                    sb.Append(Tokens.T_READS).Append(' ').Append(
                        Tokens.T_SQL).Append(' ').Append(Tokens.T_DATA);
                    break;

                case MODIFIES_SQL:
                    sb.Append(Tokens.T_MODIFIES).Append(' ').Append(
                        Tokens.T_SQL).Append(' ').Append(Tokens.T_DATA);
                    break;
            }

            return sb.ToString();
        }

        public void setReturnType(SqlType type)
        {
            returnType = type;
        }

        public SqlType getReturnType()
        {
            return returnType;
        }

        public void setTableType(SqlType[] types)
        {
            tableType = types;
        }

        public SqlType[] getTableType()
        {
            return tableType;
        }

        public void setProcedure(Statement statement)
        {
            this.statement = statement;
        }

        public Statement getProcedure()
        {
            return statement;
        }

        public void setSpecificName(QNameManager.QName name)
        {
            specificName = name;
        }

        public void setName(QNameManager.QName name)
        {
            this.name = name;
        }

        public QNameManager.QName getSpecificName()
        {
            return specificName;
        }

        public void setDeterministic(bool value)
        {
            _isDeterministic = value;
        }

        public bool isDeterministic()
        {
            return _isDeterministic;
        }

        public void setNullInputOutput(bool value)
        {
            _isNullInputOutput = value;
        }

        public bool isNullInputOutput()
        {
            return _isNullInputOutput;
        }

        public void setNewSavepointLevel(bool value)
        {
            isNewSavepointLevel = value;
        }

        public void setParameterStyle(int style)
        {
            parameterStyle = style;
        }

        public void setMethodURL(String url)
        {
            this.methodName = url;
        }

        public MethodInfo getMethod()
        {
            return cSharpMethod;
        }

        public void setMethod(MethodInfo method)
        {
            this.cSharpMethod = method;
        }

        public void setReturnTable(TableDerived table)
        {
            this.returnTable = table;
            this._returnsTable = true;
        }

        public bool returnsTable()
        {
            return _returnsTable;
        }

        public void resolve()
        {

            if (this.routineType == SchemaObjectTypes.PROCEDURE && isNewSavepointLevel
                    && dataImpact != MODIFIES_SQL)
            {
                throw Error.error(ErrorCode.X_42604);
            }

            setLanguage(language);

            if (language == Routine.LANGUAGE_SQL)
            {
                if (dataImpact == NO_SQL)
                {
                    throw Error.error(ErrorCode.X_42604);
                }

                if (parameterStyle == PARAM_STYLE_JAVA)
                {
                    throw Error.error(ErrorCode.X_42604);
                }
            }

            if (language == Routine.LANGUAGE_SQL)
            {
                if (parameterStyle != 0 && parameterStyle != PARAM_STYLE_SQL)
                {
                    throw Error.error(ErrorCode.X_42604);
                }
            }

            parameterTypes = new SqlType[parameterList.size()];
            typeGroups = 0;

            for (int i = 0; i < parameterTypes.Length; i++)
            {
                ColumnSchema param = (ColumnSchema)parameterList.get(i);

                parameterTypes[i] = param.dataType;

                if (i < 4)
                {
                    BitMap.setByte(typeGroups,
                                   (byte)param.dataType.typeComparisonGroup,
                                   i * 8);
                }
            }

            if (statement != null)
            {
                statement.resolve();
            }

            if (methodName != null && cSharpMethod == null)
            {
                bool[] hasConnection = new bool[1];

                cSharpMethod = getMethod(methodName, this, hasConnection);

                if (cSharpMethod == null)
                {
                    throw Error.error(ErrorCode.X_46103);
                }

                javaMethodWithConnection = hasConnection[0];
            }
        }

        public bool isProcedure()
        {
            return routineType == SchemaObjectTypes.PROCEDURE;
        }

        public bool isFunction()
        {
            return routineType == SchemaObjectTypes.FUNCTION;
        }

        public ColumnSchema getParameter(int i)
        {
            return (ColumnSchema)parameterList.get(i);
        }

        public SqlType[] getParameterTypes()
        {
            return parameterTypes;
        }

        public int getParameterSignature()
        {
            return typeGroups;
        }

        public int getParameterCount()
        {
            return parameterTypes.Length;
        }

        public int getParameterCount(int type)
        {

            int count = 0;

            for (int i = 0; i < parameterList.size(); i++)
            {
                ColumnSchema col = (ColumnSchema)parameterList.get(i);

                if (col.getParameterMode() == type)
                {
                    count++;
                }
            }

            return count;
        }

        public int getParameterIndex(String name)
        {
            return parameterList.getIndex(name);
        }

        public RangeVariable[] getParameterRangeVariables()
        {
            return ranges;
        }

        public int getVariableCount()
        {
            return variableCount;
        }

        public bool isLibraryRoutine()
        {
            return _isLibraryRoutine;
        }

        public QNameManager.QName[] getTableNamesForRead()
        {

            if (statement == null)
            {
                return QNameManager.QName.emptyArray;
            }

            return statement.getTableNamesForRead();
        }

        public QNameManager.QName[] getTableNamesForWrite()
        {

            if (statement == null)
            {
                return QNameManager.QName.emptyArray;
            }

            return statement.getTableNamesForWrite();
        }

        static MethodInfo getMethod(String name, Routine routine,
                                bool[] hasConnection)
        {

            int i = name.IndexOf(':');

            if (i != -1)
            {
                if (!name.Substring(0, i).Equals(SqlInvariants.CLASSPATH_NAME))
                {
                    throw Error.error(ErrorCode.X_46102, name);
                }

                name = name.Substring(i + 1);
            }

            MethodInfo method = null;
            MethodInfo[] methods = getMethods(name);

            for (i = 0; i < methods.Length; i++)
            {
                int offset = 0;
                ParameterInfo[] paras = methods[i].GetParameters();
#if !SILVERLIGHT
                if (paras.Length > 0
                        && paras[0].ParameterType.Equals(typeof(DbConnection)))
                {
                    offset = 1;
                    hasConnection[0] = true;
                }
#endif

                if (paras.Length - offset != routine.parameterTypes.Length)
                {
                    continue;
                }

                SqlType methodReturnType = SqlType.getDefaultTypeWithSize(
                    Types.getParameterSQLTypeNumber(methods[i].ReturnType));

                if (methodReturnType == null)
                {
                    continue;
                }

                if (methodReturnType.typeCode != routine.returnType.typeCode)
                {
                    continue;
                }

                method = methods[i];

                for (int j = 0; j < routine.parameterTypes.Length; j++)
                {
                    Type param = paras[j + offset].ParameterType;
                    SqlType methodParamType = SqlType.getDefaultType(
                        Types.getParameterSQLTypeNumber(param));

                    if (methodParamType == null)
                    {
                        break;
                    }

                    routine.getParameter(j).setNullable(!paras[i].ParameterType.IsPrimitive);

                    if (routine.parameterTypes[j].typeCode
                            != methodParamType.typeCode)
                    {
                        method = null;

                        break;
                    }
                }

                if (method != null)
                {
                    break;
                }
            }

            return method;
        }

        public static MethodInfo[] getMethods(String name)
        {

            int i = name.LastIndexOf('.');

            if (i == -1)
            {
                throw Error.error(ErrorCode.X_42501, name);
            }

            String classname = name.Substring(0, i);
            String methodname = name.Substring(i + 1);
            Type classinstance = null;

            try
            {
                classinstance = Type.GetType(classname);
            }
            catch (Exception t)
            {
                throw Error.error(t, ErrorCode.X_42501, ErrorCode.M_Message_Pair,
                                  new Object[] {
                t.Message, classname
            });
            }

            MethodInfo[] methods = classinstance.GetMethods();
            EfzArrayList list = new EfzArrayList();

            for (i = 0; i < methods.Length; i++)
            {
                int offset = 0;
                MethodInfo m = methods[i];
               
                if (!m.Name.Equals(methodname)
                        || !m.IsStatic
                        || !m.IsPublic)
                {
                    continue;
                }

                ParameterInfo[] paras = methods[i].GetParameters();

#if !SILVERLIGHT
                if (paras.Length > 0
                        && paras[0].ParameterType.Equals(typeof(DbConnection)))
                {
                    offset = 1;
                }
#endif

                for (int j = offset; j < paras.Length; j++)
                {
                    Type param = paras[j].ParameterType;
                    SqlType methodParamType = SqlType.getDefaultTypeWithSize(
                        Types.getParameterSQLTypeNumber(param));

                    if (methodParamType == null)
                    {
                        m = null;

                        break;
                    }
                }

                if (m == null)
                {
                    continue;
                }

                SqlType methodReturnType = SqlType.getDefaultTypeWithSize(
                    Types.getParameterSQLTypeNumber(m.ReturnType));

                if (methodReturnType != null)
                {
                    list.add(methods[i]);
                }
            }

            methods = new MethodInfo[list.size()];

            list.toArray(methods);

            return methods;
        }

        public static Routine[] newRoutines(MethodInfo[] methods)
        {

            Routine[] routines = new Routine[methods.Length];

            for (int i = 0; i < methods.Length; i++)
            {
                MethodInfo method = methods[i];

                routines[i] = newRoutine(method);
            }

            return routines;
        }

        /**
         * Returns a new function Routine object based solely on a Java Method object.
         */
        public static Routine newRoutine(MethodInfo method)
        {

            Routine routine = new Routine(SchemaObjectTypes.FUNCTION);
            int offset = 0;
            ParameterInfo[] paras = method.GetParameters();
            String className = method.DeclaringType.FullName;
            StringBuilder sb = new StringBuilder();

            sb.Append("CLASSPATH:");
            sb.Append(method.DeclaringType.FullName).Append('.');
            sb.Append(method.Name);

#if !SILVERLIGHT
            if (paras.Length > 0 && paras[0].Equals(typeof(DbConnection)))
            {
                offset = 1;
            }
#endif

            String name = sb.ToString();

            if (className.Equals("EffiProz.Core.Library")
                    || className.Equals("System.Math"))
            {
                routine._isLibraryRoutine = true;
            }

            for (int j = offset; j < paras.Length; j++)
            {
                SqlType methodParamType = SqlType.getDefaultTypeWithSize(
                    Types.getParameterSQLTypeNumber(paras[j].ParameterType));
                ColumnSchema param = new ColumnSchema(null, methodParamType,
                                                      !paras[j].ParameterType.IsPrimitive,
                                                      false, null);

                routine.addParameter(param);
            }

            routine.setLanguage(Routine.LANGUAGE_JAVA);
            routine.setMethod(method);
            routine.setMethodURL(name);
            routine.setDataImpact(Routine.NO_SQL);

            SqlType methodReturnType = SqlType.getDefaultTypeWithSize(
                Types.getParameterSQLTypeNumber(method.ReturnType));

            routine.javaMethodWithConnection = offset == 1; ;

            routine.setReturnType(methodReturnType);
            routine.resolve();

            return routine;
        }

        public static void createRoutines(Session session, QNameManager.QName schema,
                                          String name)
        {

            MethodInfo[] methods = Routine.getMethods(name);
            Routine[] routines = Routine.newRoutines(methods);
            QNameManager.QName routineName = session.database.nameManager.newHsqlName(schema,
                name, true, SchemaObjectTypes.FUNCTION);

            for (int i = 0; i < routines.Length; i++)
            {
                routines[i].setName(routineName);
                session.database.schemaManager.addSchemaObject(routines[i]);
            }
        }
    }
}
